package com.zlcx.tz.live.common.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONType;
import com.google.common.collect.Sets;
import com.zlcx.tz.live.constant.RedisKeyConst;
import com.zlcx.tz.live.utils.SpringContextHolder;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.time.LocalDateTime;
import java.util.*;

/**
 * <p></p>
 * <PRE>
 * <BR>    修改记录
 * <BR>-----------------------------------------------
 * <BR>    修改日期         修改人          修改内容
 * </PRE>
 *
 * @author zl
 * @version 1.0
 * @date Created in 2019/4/29 16:43
 * @copyright: Copyright (c) founders
 */

public class TraceLogFilter extends OncePerRequestFilter implements Ordered {

    private static Boolean NEED_TRACE = true;
    private static final String IGNORE_CONTENT_TYPE = "multipart";
    private static Timer timer;
    private static final Logger log = LoggerFactory.getLogger(TraceLogFilter.class);
    private static final int LOG_RESPONSE_BODY_MAX_LENGTH = 5 * 1024 * 1024;
    private static Set<String> STATIC_RESOURCE_PARTEN = null;


    static {
        STATIC_RESOURCE_PARTEN = Sets.newHashSet(".js", ".css", ".html", ".jpg", ".jpeg", ".png", ".gif", ".ico", ".woff");

        String key = RedisKeyConst.getFilterSwitchKey();
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                if (!SpringContextHolder.isInited()) {
                    return;
                }
                try {
                    RedisTemplate redisTemplate = SpringContextHolder.getBean("redisTemplate");
                    if (redisTemplate == null) {
                        log.warn("[TraceLogFilter] redisTemplate is null");
                        return;
                    }

                    Boolean tmpSwitch = (Boolean) redisTemplate.opsForValue().get(key);
                    if (tmpSwitch == null) {
                        redisTemplate.opsForValue().set(key, NEED_TRACE);
                    } else {
                        NEED_TRACE = tmpSwitch;
                    }
                } catch (Exception e) {
                    log.error("[TraceLogFilter] update filter switch error,e={}", e);
                }
            }
        }, 1000 * 60 * 1, 1000 * 60 * 5);

    }


    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 10;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if (!isRequestValid(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        if (!(request instanceof ContentCachingRequestWrapper)) {
            request = new ContentCachingRequestWrapper(request);
        }
        if (!(response instanceof ContentCachingResponseWrapper)) {
            response = new ContentCachingResponseWrapper(response);
        }
        int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
        long startTime = System.currentTimeMillis();
        try {
            filterChain.doFilter(request, response);
            status = response.getStatus();
        } finally {
            String path = request.getRequestURI();
            if (NEED_TRACE && !path.startsWith(IGNORE_CONTENT_TYPE) && !isStaticResource(path)) {
                HttpTraceLog traceLog = new HttpTraceLog();
                traceLog.setPath(path);
                traceLog.setMethod(request.getMethod());
                traceLog.setIp(getIpAddr(request));
                traceLog.setCostTime(System.currentTimeMillis() - startTime);
                traceLog.setTimeStamp(LocalDateTime.now().toString());
                traceLog.setParameters(JSON.toJSONString(request.getParameterMap()));
                Map<String, String> headers = new HashMap<>(10);
                Enumeration<String> headNames = request.getHeaderNames();
                while (headNames.hasMoreElements()) {
                    String name = headNames.nextElement();
                    String value = request.getHeader(name);
                    if (StringUtils.isNotBlank(value)) {
                        headers.put(name, value);
                    }
                }
                traceLog.setHeaders(JSON.toJSONString(headers));
                traceLog.setStatus(status);
                traceLog.setRequestBody(getRequestBody(request));
                String responseBody = getResponseBody(response);
                if (responseBody != null && responseBody.getBytes().length > LOG_RESPONSE_BODY_MAX_LENGTH) {
                    return;
                }
                traceLog.setResponseBody(responseBody);
                //log.info("trace log: {}", JSON.toJSONString(traceLog));
            }
            updateResponse(response);
        }
    }

    private boolean isRequestValid(HttpServletRequest request) {
        try {
            new URL(request.getRequestURL().toString());
            return true;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    private boolean isStaticResource(String requestURI) {
        if (StringUtils.isBlank(requestURI) || requestURI.length() < 2) {
            return false;
        }
        return STATIC_RESOURCE_PARTEN.parallelStream().anyMatch(parten -> requestURI.endsWith(parten));
    }


    private String getRequestBody(HttpServletRequest request) {
        String requestBody = "";

        ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            try {
                requestBody = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
            } catch (IOException e) {
            }
        }
        return requestBody;
    }

    private String getResponseBody(HttpServletResponse response) {
        String responseBody = "";
        ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                responseBody = IOUtils.toString(wrapper.getContentAsByteArray(), wrapper.getCharacterEncoding());
            } catch (IOException e) {
            }
        }
        return responseBody;
    }

    private void updateResponse(HttpServletResponse response) throws IOException {
        ContentCachingResponseWrapper responseWrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        Objects.requireNonNull(responseWrapper).copyBodyToResponse();
    }

    public String getIpAddr(HttpServletRequest request) {
        String ip = request.getHeader("X-Real-IP");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("x-forwarded-for");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader(" Proxy-Client-IP ");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader(" WL-Proxy-Client-IP ");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }


    @Data
    @JSONType(orders = {"status", "costTime", "path", "method", "ip", "headers","parameters", "requestBody", "responseBody", "timeStamp"})
    private static class HttpTraceLog {

        private String timeStamp;
        private String path;
        private String method;
        private String ip;
        private String headers;
        private String parameters;
        private String requestBody;
        private Integer status;
        private Long costTime;
        private String responseBody;
    }
}
